<?php
/**
 * Redis锁操作,解决并发问题
 * @author zhiyong.luo
 * @date 2017-04-08
 */
include_once dirname(__FILE__).'/np_string.php';
class np_redis_lock_class 
{
	private $_redis = null;

	/** 
	 * @param object $redis redis 连接对象
	 * @author zhiyong.luo
	 * @date 2017-04-08
	 */
	public function __construct($redis)
	{
		$this->_redis = $redis;
	}
	/**
	 * 
	 * 加锁
	 * @param  string  $name           锁的标识名
	 * @param  integer $timeout        循环获取锁的等待超时时间，在此时间内会一直尝试获取锁直到超时，为0表示失败后直接返回不等待
	 * @param  integer $expire         当前锁的最大生存时间(秒)，必须大于0，如果超过生存时间锁仍未被释放，则系统会自动强制释放
	 * @param  integer $wait_interval_us 获取锁失败后挂起再试的时间间隔(微秒)
	 * @author  chaozhong.leng <chaozhong.leng@starcor.cn>
	 * @date 2016年8月24日 下午8:51:25
	 */
	public function lock($name, $timeout = 0, $expire = 10, $wait_interval_us = 100000) 
	{
		if ($name == null) 
		{
			return false;
		}
	
		//取得当前时间
		$now = time();
		//获取锁失败时的等待超时时刻
		$timeoutAt = $now + $timeout;
		//锁的最大生存时刻
		$expireAt = $expire;
		$redisKey = "Lock:{$name}";
		//随机标识符
		$guid = np_guid_rand($redisKey);
		
		while (true) 
		{
			//设置rediskey
			$result = $this->_redis->setnx($redisKey, $guid);
            //设置KEY成功，再设置KEY的失效时间，如果redis中存在当前KEY,则设置失败
			if ($result != false)
			{
				$this->_redis->expire($redisKey, $expireAt);
				return $guid;
			}
			//以秒为单位，返回给定key的剩余生存时间
			$ttl = $this->_redis->ttl($redisKey);

			//ttl小于0 表示key上没有设置生存时间（key是不会不存在的，因为前面setnx会自动创建）
			//ttl等于0 redis 在主从复制上可能会出现这个问题（芒果tv曾出现）
			//如果出现这种状况，那就是进程的某个实例setnx成功后 crash 导致紧跟着的expire没有被调用
			//这时可以直接设置expire并把锁纳为己用
			if ($ttl <= 0) 
			{
				$result = $this->_redis->set($redisKey, $guid);
				if($result)
				{
					$this->_redis->expire($redisKey, $expireAt);
					return $guid;
				}
				
			}
	
			/*****循环请求锁部分*****/
			//如果没设置锁失败的等待时间 或者 已超过最大等待时间了，那就退出
			if ($timeout <= 0 || $timeoutAt < microtime(true))
			{
				break;
			}
	
			//隔 $wait_interval 后继续 请求
			usleep($wait_interval_us);
	
		}
	
		return false;
	}
	
	/**
	 * 解锁
	 * @param  string $name
	 * @return boolean
	 * @author  chaozhong.leng <chaozhong.leng@starcor.cn>
	 * @date 2016年8月24日 下午8:59:08
	 */
	public function unlock($name) 
	{
		$lock_name = "Lock:$name";
		return $this->_redis->delete($lock_name);
	}
}